﻿/*
Copyright (c) 2013, Bartłomiej Gałkowski
All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met: 

1. Redistributions of source code must retain the above copyright notice, this
   list of conditions and the following disclaimer. 
2. Redistributions in binary form must reproduce the above copyright notice,
   this list of conditions and the following disclaimer in the documentation
   and/or other materials provided with the distribution. 

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/

using System;
using System.CodeDom;
using System.CodeDom.Compiler;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Reflection;
using System.Text;
using Microsoft.CSharp;
using Microsoft.VisualBasic;

namespace PyroGen
{

    public sealed class StringTextWriter : StreamWriter
    {
        private static string _strtext = "";

        public StringTextWriter()
            : base(new StringOutStream(), Encoding.Unicode, 1024)
        {
            _strtext = "";
            AutoFlush = true;
        }


        public string GetString(string language)
        {
            int strip = 404;
            if(language.StartsWith("vb.net"))
                strip = 398;
            return _strtext.Substring(strip, _strtext.Length - strip);
        }

        #region Nested type: StringOutStream

        private class StringOutStream : Stream
        {
            public override bool CanRead
            {
                get { return false; }
            }

            public override bool CanSeek
            {
                get { return false; }
            }

            public override bool CanWrite
            {
                get { return true; }
            }

            public override long Length
            {
                get { throw new InvalidOperationException(); }
            }

            public override long Position
            {
                get { throw new InvalidOperationException(); }
                set { throw new InvalidOperationException(); }
            }

            public override void Write(byte[] buffer, int offset, int count)
            {
                _strtext += Encoding.Unicode.GetString(buffer, offset, count);
                //  Debug.Write(Encoding.Unicode.GetString(buffer, offset, count));
                //  Debug.Flush();
            }

            public override void Flush()
            {
                Debug.Flush();
            }

            public override int Read(byte[] buffer, int offset, int count)
            {
                throw new InvalidOperationException();
            }

            public override long Seek(long offset, SeekOrigin origin)
            {
                throw new InvalidOperationException();
            }

            public override void SetLength(long value)
            {
                throw new InvalidOperationException();
            }
        };

        #endregion
    }


    public class AbstractCode
    {
        private const string MainMethodName = "TextTransform";
        private const string MainNamespaceName = "GeneratedCodeNamespace";
        private const string MainClassName = "GeneratedClass";


        private readonly List<ConfigHandler> _configs;
        private readonly CodeCompileUnit _mainUnit;
        private readonly List<Snippet> _snippets;

        public AbstractCode(IEnumerable<Snippet> codeDescription)
        {
            _snippets = new List<Snippet>(codeDescription);
            _configs = new List<ConfigHandler>();
            _mainUnit = GenerateAbstractCodeClass();
        }

        public bool ExecutionFailed { get; private set; }


        /// <summary>
        /// Generates the abstract CodeDOM from the snippets ready to be executed. Has whole class method extension. 
        ///  Not tested.
        /// </summary>
        /// <returns></returns>
        private CodeCompileUnit GenerateAbstractCodeClass()
        {
            // Creation of the main method "PyrogenMain" of the main class "PyroGenClass".
            CodeMemberMethod mainMethod = new CodeMemberMethod();
            mainMethod.Name = MainMethodName;
            mainMethod.ReturnType = new CodeTypeReference("System.String");
            mainMethod.Attributes = MemberAttributes.Public;

            // Creation of the StringBuilder object "PyroGenOut" witch string (PyroGenOut.ToString()) is returned by the main method.
            var typeStringBuilder = new CodeTypeReferenceExpression("System.Text.StringBuilder");
            CodeVariableDeclarationStatement outVar = new CodeVariableDeclarationStatement(typeof (StringBuilder),
                                                                                           "PyroGenOut",
                                                                                           new CodeObjectCreateExpression
                                                                                               (typeof (StringBuilder)));
            mainMethod.Statements.Add(outVar);
            CodeVariableReferenceExpression typeOutVar = new CodeVariableReferenceExpression("PyroGenOut");

            // The main class in witch the TextTransform method resides
            CodeTypeDeclaration mainClass = new CodeTypeDeclaration(MainClassName);
            mainClass.Members.Add(mainMethod);


            // The main namespace in witch the main class resides
            CodeNamespace mainNamespace = new CodeNamespace(MainNamespaceName);

            mainNamespace.Imports.Add(new CodeNamespaceImport("System"));
            mainNamespace.Types.Add(mainClass);
            mainNamespace.Comments.Clear();


            // Each addin/metacode snippet is handled here and added as a statement to the main method.
            foreach (Snippet snippet in _snippets)
            {
                switch (snippet.Type)
                {
                    case CodeType.Text:
                        CodeMethodInvokeExpression codeText = new CodeMethodInvokeExpression(
                            typeOutVar, "Append", new CodePrimitiveExpression(snippet.Text));
                        mainMethod.Statements.Add(codeText);
                        break;

                    case CodeType.Expression:
                        CodeMethodInvokeExpression codeExpr = new CodeMethodInvokeExpression(
                            typeOutVar, "Append", new CodeSnippetExpression(snippet.Text));
                        mainMethod.Statements.Add(codeExpr);
                        break;

                    case CodeType.Code:
                        CodeSnippetStatement frag = new CodeSnippetStatement(snippet.Text);
                        mainMethod.Statements.Add(frag);
                        break;

                    case CodeType.ConfigCode:
                        ConfigHandler config = new ConfigHandler(snippet.Text);
                        if (MainScreen.Console != null)
                            MainScreen.Console.WriteLine("AbstractCode: Config Code Found");


                        if (config.IsProperty())
                        {
                            Property prop = config.GetProperty();

                            if (prop.IsTyped())
                            {
                                CodeMemberField field = new CodeMemberField(prop.Type, "___" + prop.Name);
                                mainClass.Members.Add(field);
                                CodeMemberProperty property = new CodeMemberProperty();
                                property.Name = prop.Name;
                                property.Type = new CodeTypeReference(prop.Type);
                                property.GetStatements.Add(
                                    new CodeMethodReturnStatement(
                                        new CodeFieldReferenceExpression(new CodeThisReferenceExpression(),
                                                                         "___" + prop.Name)));
                                property.SetStatements.Add(
                                    new CodeAssignStatement(
                                        new CodeFieldReferenceExpression(new CodeThisReferenceExpression(),
                                                                         "___" + prop.Name),
                                        new CodePropertySetValueReferenceExpression()));


                                property.Attributes = MemberAttributes.Public;
                                mainClass.Members.Add(property);
                            }

                        }
                        else
                        {
                            // Adding namespace imports a.k.a using clauses (if any are present)
                            foreach (string namsp in config.GetNamespaces())
                                mainNamespace.Imports.Add(new CodeNamespaceImport(namsp));

                            // main config add
                            _configs.Add(config);
                        }

                        break;

                    case CodeType.ScriptCode:
                        mainClass.Members.Add(new CodeSnippetTypeMember(snippet.Text));
                        break;
                }
            }

            // The return PyroGenOut.ToString(); statement of the main method.
            CodeMethodReturnStatement returnState = new CodeMethodReturnStatement(new CodeMethodInvokeExpression(
                                                                                      typeOutVar, "ToString"));
            mainMethod.Statements.Add(returnState);


            // The main compile unit
            CodeCompileUnit unit = new CodeCompileUnit();
            unit.Namespaces.Add(mainNamespace);


            return unit;
        }

        /// <summary>
        /// Writes the code generated from a CodeCompileUnit to output.
        /// </summary>
        /// <param name="compileunit"></param>
        public string GenerateCodeText(bool forceBeautify)
        {
            var beautify = forceBeautify || ShouldBeBeautified();


            var providerOptions = new Dictionary<string, string>
                                      {
                                          {"CompilerVersion", "v4.0"}
                                      };

            StringTextWriter textWriter = new StringTextWriter();

            

            if (WhatLanguageIsTheCode().StartsWith("vb.net"))
            {
                VBCodeProvider provider = new VBCodeProvider(providerOptions);
                provider.GenerateCodeFromCompileUnit(_mainUnit, textWriter, new CodeGeneratorOptions());
            }
            else
            {
                CSharpCodeProvider provider = new CSharpCodeProvider(providerOptions);
                provider.GenerateCodeFromCompileUnit(_mainUnit, textWriter, new CodeGeneratorOptions());
            }
            

            
            textWriter.Close();
            return textWriter.GetString(WhatLanguageIsTheCode());
        }


        public bool ShouldBeExecuted()
        {
            // Each addin/metacode snippet is handled here and added as a statement to the main method.
            foreach (Snippet snippet in _snippets)

                if (snippet.Type == CodeType.ConfigCode)
                {
                        ConfigHandler config = new ConfigHandler(snippet.Text);

                        string execute = config.FindValueOfConfig(SettingsLoader.GetExecuteKeyword());
                        if (execute == "true" || execute == "yes")
                            return true;
                        if (execute == "false" || execute == "no")
                            return false;

                }
            return false;
        }

        public bool ShouldBeBeautified()
        {
            foreach (ConfigHandler config in _configs)
            {
                string beautify = config.FindValueOfConfig(SettingsLoader.GetBeautifyKeyword());
                if (beautify == "false")
                    return false;

                if (beautify == "true")
                    return true;

            }

            return false;
        }

        public string WhatLanguageIsTheCode()
        {
            foreach (ConfigHandler config in _configs)
            {
                string lang = config.FindValueOfConfig(SettingsLoader.GetLanguageKeyword());
                if (lang != null && lang.ToLower().Contains("vb"))
                    return "vb.net";
            }

            return "c#";
        }

        public string RunCode()
        {
            var providerOptions = new Dictionary<string, string>
                                      {
                                          {"CompilerVersion", "v4.0"}
                                      };


            CompilerParameters compilerParams = new CompilerParameters
                                                    {GenerateInMemory = true, GenerateExecutable = false};


            CompilerResults results;
            
            // add assemblies to the code
            foreach (ConfigHandler codeConfigHandler in _configs)
                foreach (string assm in codeConfigHandler.GetAssemblies())
                    compilerParams.ReferencedAssemblies.Add(assm);
            
            if (WhatLanguageIsTheCode().StartsWith("vb.net"))
            {
          
                VBCodeProvider provider = new VBCodeProvider(providerOptions);
                results = provider.CompileAssemblyFromDom(compilerParams, _mainUnit);
            }
            else
            {
                CSharpCodeProvider provider = new CSharpCodeProvider(providerOptions);
                results = provider.CompileAssemblyFromDom(compilerParams, _mainUnit);
            }

            if (results.Errors.Count > 0)
            {
                Debug.WriteLine("Warning RunCode: compile errors !");
                string errors = "";
                foreach (CompilerError er in results.Errors)
                {
                    Debug.WriteLine(er.ToString());
                    errors += er.ToString();
                    ExecutionFailed = true;
                }
                return errors;
            }
            ExecutionFailed = false;
            object instance = results.CompiledAssembly.CreateInstance(MainNamespaceName + "." + MainClassName);
            Debug.Assert(instance != null, "instance != null");
            MethodInfo mi = instance.GetType().GetMethod(MainMethodName);
            try
            {
                return mi.Invoke(instance, null).ToString();
            }
            catch (Exception e)
            {
                ExecutionFailed = true;
                return "Problem executing the assembly:\n" + e.Message + "\n" +e.InnerException.Message;;
            }
        }
    }
}